#region .NET Disclaimer/Info

//===============================================================================
//
// gOODiDEA, uland.com
//===============================================================================
//
// $Header :		$  
// $Author :		$
// $Date   :		$
// $Revision:		$
// $History:		$  
//  
//===============================================================================

#endregion

#region Java

/**
 * Class AnimatedGifEncoder - Encodes a GIF file consisting of one or
 * more frames.
 * <pre>
 * Example:
 *    AnimatedGifEncoder e = new AnimatedGifEncoder();
 *    e.start(outputFileName);
 *    e.setDelay(1000);   // 1 frame per sec
 *    e.addFrame(image1);
 *    e.addFrame(image2);
 *    e.finish();
 * </pre>
 * No copyright asserted on the source code of this class.  May be used
 * for any purpose, however, refer to the Unisys LZW patent for restrictions
 * on use of the associated LZWEncoder class.  Please forward any corrections
 * to kweiner@fmsware.com.
 *
 * @author Kevin Weiner, FM Software
 * @version 1.03 November 2003
 *
 */

#endregion

using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;

namespace CloudShot.Utils.Encoding
{
  public sealed class AnimatedGifEncoder
  {
    private readonly int _width; // image size
    private readonly int _height;
    private Color _transparent = Color.Empty; // transparent color if given
    private int _transIndex; // transparent index in color table
    private int _repeat = -1; // no repeat
    private int _delay; // frame delay (hundredths)
    private bool _started; // ready to output frames
    private MemoryStream _memoryStream;

    private byte[] _pixels; // BGR byte array from frame
    private byte[] _indexedPixels; // converted frame indexed to palette
    private int _colorDepth; // number of bit planes
    private byte[] _colorTab; // RGB palette
   
    private int _palSize = 7; // color table size (bits-1)
    private int _dispose = -1; // disposal code (-1 = use default)
    private bool _closeStream; // close stream when finished
    private bool _firstFrame = true;
    private int _sample = 10; // default sample interval for quantizer

    public AnimatedGifEncoder(int width, int height, int fps)
    {
      _width = width;
      _height = height;
      if (_width < 1)
        _width = 320;
      if (_height < 1)
        _height = 240;

      _pixels = new Byte[3 * _width * _height];
      _indexedPixels = new byte[_width * _height];

      if (fps > 0f)
        _delay = (int)Math.Round(100f / fps, MidpointRounding.AwayFromZero);
    }


    /**
		 * Sets the delay time between each frame, or changes it
		 * for subsequent frames (applies to last frame added).
		 *
		 * @param ms int delay time in milliseconds
		 */

    public void SetDelay(int ms)
    {
      _delay = (int)Math.Round(ms / 10.0f, MidpointRounding.AwayFromZero);
    }

    /**
		 * Sets the GIF frame disposal code for the last added frame
		 * and any subsequent frames.  Default is 0 if no transparent
		 * color has been set, otherwise 2.
		 * @param code int disposal code.
		 */

    public void SetDispose(int code)
    {
      if (code >= 0)
        _dispose = code;
    }

    /**
		 * Sets the number of times the set of GIF frames
		 * should be played.  Default is 1; 0 means play
		 * indefinitely.  Must be invoked before the first
		 * image is added.
		 *
		 * @param iter int number of iterations.
		 * @return
		 */

    public void SetRepeat(int iter)
    {
      if (iter >= 0)
        _repeat = iter;
    }

    /**
		 * Sets the transparent color for the last added frame
		 * and any subsequent frames.
		 * Since all colors are subject to modification
		 * in the quantization process, the color in the final
		 * palette for each frame closest to the given color
		 * becomes the transparent color for that frame.
		 * May be set to null to indicate no transparent color.
		 *
		 * @param color Color to be treated as transparent on display.
		 */

    public void SetTransparent(Color color)
    {
      _transparent = color;
    }

    /**
		 * Adds next GIF frame.  The frame is not written immediately, but is
		 * actually deferred until the next frame is received so that timing
		 * data can be inserted.  Invoking <code>finish()</code> flushes all
		 * frames.  If <code>setSize</code> was not invoked, the size of the
		 * first image is used for all subsequent frames.
		 *
		 * @param image BufferedImage containing frame to write.
		 * @return true if successful.
		 */

    public bool AddFrame(Bitmap bitmap)
    {
      if ((bitmap == null) || !_started)
      {
        return false;
      }
      bool ok = true;
      try
      {
       
        //FillImagePixels(_pixels, bitmap); // convert to correct format if necessary
        FillImagePixelsFast(_pixels, bitmap); 
        AnalyzePixels(_pixels, _indexedPixels); // build color table & map pixels
        if (_firstFrame)
        {
          WriteLSD(); // logical screen descriptior
          WritePalette(); // global color table
          if (_repeat >= 0)
          {
            // use NS app extension to indicate reps
            WriteNetscapeExt();
          }
        }
        WriteGraphicCtrlExt(); // write graphic control extension
        WriteImageDesc(); // image descriptor
        if (!_firstFrame)
        {
          WritePalette(); // local color table
        }
        WritePixels(); // encode and write pixel data
        _firstFrame = false;
      }
      catch (IOException)
      {
        ok = false;
      }

      return ok;
    }

    /**
		 * Flushes any pending data and closes output file.
		 * If writing to an OutputStream, the stream is not
		 * closed.
		 */

    public Image Finish()
    {
      if (!_started)
        return null;

      bool ok = true;
      _started = false;
      Image image = null;

      try
      {
        _memoryStream.WriteByte(0x3b); // gif trailer
        _memoryStream.Flush();
        image = Image.FromStream(_memoryStream);

        if (_closeStream)
        {
          _memoryStream.Close();
        }
      }
      catch (IOException)
      {
        ok = false;
      }

      // reset for subsequent use
      _transIndex = 0;
      _memoryStream = null;
      _pixels = null;
      _indexedPixels = null;
      _colorTab = null;
      _closeStream = false;
      _firstFrame = true;

      if (ok)
        return image;

      return null;
    }

    /**
		 * Sets frame rate in frames per second.  Equivalent to
		 * <code>setDelay(1000/fps)</code>.
		 *
		 * @param fps float frame rate (frames per second)
		 */

    /**
		 * Sets quality of color quantization (conversion of images
		 * to the maximum 256 colors allowed by the GIF specification).
		 * Lower values (minimum = 1) produce better colors, but slow
		 * processing significantly.  10 is the default, and produces
		 * good color mapping at reasonable speeds.  Values greater
		 * than 20 do not yield significant improvements in speed.
		 *
		 * @param quality int greater than 0.
		 * @return
		 */

    public void SetQuality(int quality)
    {
      if (quality < 1)
        quality = 1;
      _sample = quality;
    }

    /**
		 * Sets the GIF frame size.  The default size is the
		 * size of the first frame added if this method is
		 * not invoked.
		 *
		 * @param w int frame width.
		 * @param h int frame width.
		 */

 
    public bool Start()
    {
      bool ok;
      try
      {
        _memoryStream = new MemoryStream();
        ok = Start(_memoryStream);
        _closeStream = true;
      }
      catch (IOException)
      {
        ok = false;
      }
      return _started = ok;
    }

    private bool Start(MemoryStream fileStream)
    {
      if (fileStream == null)
        return false;
      bool ok = true;
      _closeStream = false;
      _memoryStream = fileStream;
      try
      {
        WriteString("GIF89a"); // header
      }
      catch (IOException)
      {
        ok = false;
      }
      return _started = ok;
    }

    /**
		 * Analyzes image colors and creates color map.
		 */

    private void AnalyzePixels(byte[] pixels, byte[] indexedPixels)
    {
      int len = pixels.Length;
      int nPix = len / 3;
      var nq = new NeuQuant(pixels, len, _sample);
      // initialize quantizer
      _colorTab = nq.Process(); // create reduced palette
     
      int k = 0;

      bool[] usedEntry = new bool[256]; //here is the fix. from the internet, codeproject // active palette entries
      for (int i = 0; i < nPix; i++)
      {
        int index = nq.Map(pixels[k++] & 0xff, pixels[k++] & 0xff, pixels[k++] & 0xff);
        usedEntry[index] = true;
        indexedPixels[i] = (byte)index;
      }
      //_pixels = null;
      _colorDepth = 8;
      _palSize = 7;
      // get closest match to transparent color if specified
      if (_transparent != Color.Empty)
      {
        _transIndex = FindClosest(_transparent, _colorTab, usedEntry);
      }
    }

    /**
		 * Returns index of palette color closest to color
		 *
		 */

    private static int FindClosest(Color c, byte[] colorTab,  bool[] usedEntry)
    {
      if (colorTab == null)
        return -1;

      int r = c.R;
      int g = c.G;
      int b = c.B;
      int minpos = 0;
      int dmin = 256 * 256 * 256;
      int len = colorTab.Length;
      for (int i = 0; i < len;)
      {
        int dr = r - (colorTab[i++] & 0xff);
        int dg = g - (colorTab[i++] & 0xff);
        int db = b - (colorTab[i] & 0xff);
        int d = dr * dr + dg * dg + db * db;
        int index = i / 3;
        if (usedEntry[index] && (d < dmin))
        {
          dmin = d;
          minpos = index;
        }
        i++;
      }
      return minpos;
    }

    /**
		 * Extracts image pixels into byte array "pixels"
		 */

    private static void FillImagePixels(byte[] pixels, Bitmap bitmap)
    {
      int count = 0;
      for (int th = 0; th < bitmap.Height; th++)
      {
        for (int tw = 0; tw < bitmap.Width; tw++)
        {
          Color color = bitmap.GetPixel(tw, th);
          pixels[count] = color.R;
          count++;
          pixels[count] = color.G;
          count++;
          pixels[count] = color.B;
          count++;
        }
      }
    }

    private static byte[] sourcePixels;

    private static void FillImagePixelsFast(byte[] pixels, Bitmap bitmap)
    {
      //
      // improved performance
      Bitmap tempBitmap = bitmap;
      BitmapData bitmapData = null;
      try
      {
        bitmapData = tempBitmap.LockBits(
              new Rectangle(0, 0, tempBitmap.Width, tempBitmap.Height)
            , ImageLockMode.ReadOnly
            , PixelFormat.Format24bppRgb
            );
        IntPtr pixelAddress = bitmapData.Scan0;
        int physicalLineLength = bitmapData.Stride;

        if (sourcePixels == null || sourcePixels.Length != physicalLineLength * tempBitmap.Height)
        {
          //Debug.Write("new allocation");
          sourcePixels = new byte[physicalLineLength * tempBitmap.Height];
        }
        Marshal.Copy(
              pixelAddress
            , sourcePixels
            , 0
            , sourcePixels.Length
            );
        int sourceIndex = 0;
        int targetIndex = 0;
        int logicalLineLength = 3 * tempBitmap.Width;
        
        for (int th = 0; th < tempBitmap.Height; th++)
        {
          Array.Copy(
                sourcePixels
              , sourceIndex
              , pixels
              , targetIndex
              , logicalLineLength
              );
          sourceIndex += bitmapData.Stride;
          targetIndex += logicalLineLength;
        }
        for (int tw = 0; tw <= pixels.Length - 3; tw += 3)
        {
          byte tempColor = pixels[tw];
          pixels[tw] = pixels[tw + 2];
          pixels[tw + 2] = tempColor;
        }
      }
      finally
      {
        if (bitmapData != null)
        {
          tempBitmap.UnlockBits(bitmapData);
        }
      }
    }


    /**
		 * Writes Graphic Control Extension
		 */

    private void WriteGraphicCtrlExt()
    {
      _memoryStream.WriteByte(0x21); // extension introducer
      _memoryStream.WriteByte(0xf9); // GCE label
      _memoryStream.WriteByte(4); // data block size
      int transp, disp;
      if (_transparent == Color.Empty)
      {
        transp = 0;
        disp = 0; // dispose = no action
      }
      else
      {
        transp = 1;
        disp = 2; // force clear if using transparent color
      }
      if (_dispose >= 0)
      {
        disp = _dispose & 7; // user override
      }
      disp <<= 2;

      // packed fields
      _memoryStream.WriteByte(Convert.ToByte(0 | // 1:3 reserved
                                             disp | // 4:6 disposal
                                             0 | // 7   user input - 0 = none
                                             transp)); // 8   transparency flag

      WriteShort(_delay); // delay x 1/100 sec
      _memoryStream.WriteByte(Convert.ToByte(_transIndex)); // transparent color index
      _memoryStream.WriteByte(0); // block terminator
    }

    /**
		 * Writes Image Descriptor
		 */

    private void WriteImageDesc()
    {
      _memoryStream.WriteByte(0x2c); // image separator
      WriteShort(0); // image position x,y = 0,0
      WriteShort(0);
      WriteShort(_width); // image size
      WriteShort(_height);
      // packed fields
      if (_firstFrame)
      {
        // no LCT  - GCT is used for first (or only) frame
        _memoryStream.WriteByte(0);
      }
      else
      {
        // specify normal LCT
        _memoryStream.WriteByte(Convert.ToByte(0x80 | // 1 local color table  1=yes
                                               0 | // 2 interlace - 0=no
                                               0 | // 3 sorted - 0=no
                                               0 | // 4-5 reserved
                                               _palSize)); // 6-8 size of color table
      }
    }

    /**
		 * Writes Logical Screen Descriptor
		 */

    private void WriteLSD()
    {
      // logical screen size
      WriteShort(_width);
      WriteShort(_height);
      // packed fields
      _memoryStream.WriteByte(Convert.ToByte(0x80 | // 1   : global color table flag = 1 (gct used)
                                             0x70 | // 2-4 : color resolution = 7
                                             0x00 | // 5   : gct sort flag = 0
                                             _palSize)); // 6-8 : gct size

      _memoryStream.WriteByte(0); // background color index
      _memoryStream.WriteByte(0); // pixel aspect ratio - assume 1:1
    }

    /**
		 * Writes Netscape application extension to define
		 * repeat count.
		 */

    private void WriteNetscapeExt()
    {
      _memoryStream.WriteByte(0x21); // extension introducer
      _memoryStream.WriteByte(0xff); // app extension label
      _memoryStream.WriteByte(11); // block size
      WriteString("NETSCAPE" + "2.0"); // app id + auth code
      _memoryStream.WriteByte(3); // sub-block size
      _memoryStream.WriteByte(1); // loop sub-block id
      WriteShort(_repeat); // loop count (extra iterations, 0=repeat forever)
      _memoryStream.WriteByte(0); // block terminator
    }

    /**
		 * Writes color table
		 */

    private void WritePalette()
    {
      _memoryStream.Write(_colorTab, 0, _colorTab.Length);
      int n = (3 * 256) - _colorTab.Length;
      for (int i = 0; i < n; i++)
      {
        _memoryStream.WriteByte(0);
      }
    }

    /**
		 * Encodes and writes pixel data
		 */

    private void WritePixels()
    {
      var encoder = new LZWEncoder(_width, _height, _indexedPixels, _colorDepth);
      encoder.Encode(_memoryStream);
    }

    /**
		 *    Write 16-bit value to output stream, LSB first
		 */

    private void WriteShort(int value)
    {
      _memoryStream.WriteByte(Convert.ToByte(value & 0xff));
      _memoryStream.WriteByte(Convert.ToByte((value >> 8) & 0xff));
    }

    /**
		 * Writes string to output stream
		 */

    private void WriteString(String s)
    {
      char[] chars = s.ToCharArray();
      for (int i = 0; i < chars.Length; i++)
      {
        _memoryStream.WriteByte((byte)chars[i]);
      }
    }
  }
}